package cn.com.zero.file.client;

import cn.com.zero.file.config.properties.FileUploadProperties;
import cn.com.zero.file.util.FileUtil;
import cn.com.zero.file.vo.FileInfo;
import cn.hutool.core.io.resource.ResourceUtil;
import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.Item;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

/**
 * @author liuxh
 * @version 1.0.0
 * @Description MinioTemplate
 * @createTime 2022/9/16
 */
public class MinioTemplate {
    private final Logger log = LoggerFactory.getLogger(this.getClass());

    // 公共读存储桶策略路径
    private final static String POLICY_PATH = "file/bucket-custom-policy.json";
    // 默认的自定义公共存储桶名称
    public static final String BUCKET_DEFAULT = "bucket-custom";
    // 默认的私有存储桶名称
    public static final String BUCKET_DEFAULT_PRIVATE = "bucket-private";

    private MinioClient minioClient;
    private FileUploadProperties fileUploadProperties;

    /**
     * 构造函数
     *
     * @param minioClient minio客户端
     */
    public MinioTemplate(MinioClient minioClient, FileUploadProperties fileUploadProperties) {
        this.minioClient = minioClient;
        this.fileUploadProperties = fileUploadProperties;
    }


    /**
     * 初始化默认存储桶
     */
    @PostConstruct
    public void initDefaultBucket() {
        // 私有
        if (bucketExists(BUCKET_DEFAULT_PRIVATE)) {
            log.info("默认私有存储桶：{}已存在", BUCKET_DEFAULT_PRIVATE);
            log.info("默认私有存储桶策略：{}", getBucketPolicy(BUCKET_DEFAULT_PRIVATE));
        } else {
            log.info("创建默认私有存储桶：{}", BUCKET_DEFAULT_PRIVATE);
            createBucket(BUCKET_DEFAULT_PRIVATE);
        }
        // 自定义
        if (bucketExists(BUCKET_DEFAULT)) {
            log.info("默认公共读存储桶：{}已存在", BUCKET_DEFAULT);
            log.info("默认公共存读储桶策略：{}", getBucketPolicy(BUCKET_DEFAULT));
        } else {
            log.info("创建默认公共读存储桶：{}", BUCKET_DEFAULT);
            createBucket(BUCKET_DEFAULT);

            try {
                String str = ResourceUtil.readUtf8Str(POLICY_PATH);
                log.info("设置公共读存储桶策略，策略为：{}", str);
                // 【注】当给存储桶权限策略设置为公共读写的时候，建议不要给ListBucket权限，避免数据泄露，因为公共读的桶直接访问桶的时候会直接返回桶内的所有文件信息
                setBucketPolicy(BUCKET_DEFAULT, str);
            } catch (Exception e) {
                // 设置策略失败，删除存储桶
                removeBucket(BUCKET_DEFAULT, false);
                log.info("设置公共读存储桶策略失败，删除存储桶：{}", BUCKET_DEFAULT);
            }
        }
    }


    /**
     * 存储桶是否存在
     *
     * @param bucketName 桶名
     * @return 是否存在
     */
    public boolean bucketExists(String bucketName) {
        try {
            return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException
                | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            log.error("查询存储桶是否存在发生异常，异常信息：", e);
            throw new RuntimeException("查询存储桶是否存在未知异常", e);
        }
    }

    /**
     * 创建存储桶
     *
     * @param bucketName 桶名
     */
    public void createBucket(String bucketName) {
        try {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException
                | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            log.error("创建存储桶存在发生异常，异常信息：", e);
            throw new RuntimeException("创建存储桶未知异常", e);
        }
    }

    /**
     * 设置存储桶策略
     *
     * @param bucketName   存储桶名称
     * @param policyConfig 策略配置json串
     */
    public void setBucketPolicy(String bucketName, String policyConfig) {
        try {
            minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucketName).config(policyConfig).build());
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            log.error("设置存储桶策略发生异常，异常信息：", e);
            throw new RuntimeException("设置存储桶策略未知异常", e);
        }
    }


    /**
     * 获取存储桶策略
     *
     * @param bucketName 桶名
     * @return 是否存在
     */
    public String getBucketPolicy(String bucketName) {
        try {
            return minioClient.getBucketPolicy(GetBucketPolicyArgs.builder().bucket(bucketName).build());
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException | BucketPolicyTooLargeException e) {
            log.error("查询存储桶策略发生异常，异常信息：", e);
            throw new RuntimeException("查询存储桶策略未知异常", e);
        }
    }

    /**
     * 删除一个存储桶
     *
     * @param bucketName 桶名
     */
    public void removeBucket(String bucketName) {
        // 是否为空桶  如果存储桶存在对象不为空时，直接删除会报错。
        List<String> list = listObjectNames(bucketName);
        removeBucket(bucketName, !CollectionUtils.isEmpty(list));
    }

    /**
     * 删除存储桶
     * 桶为空时直接删除； 桶不为空时先删除桶中的数据，然后再删除桶
     *
     * @param bucketName    存储桶名
     * @param bucketNotNull 是否不是空存储桶 true：不是空 false： 是空
     */
    public void removeBucket(String bucketName, boolean bucketNotNull) {
        try {
            if (bucketNotNull) {
                deleteBucketAllObject(bucketName);
            }
            minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException
                | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            log.error("删除存储桶存在发生异常，异常信息：", e);
            throw new RuntimeException("删除存储桶未知异常", e);
        }
    }


    /**
     * 删除桶中所有的对象
     *
     * @param bucketName 桶对象
     */
    public void deleteBucketAllObject(String bucketName) {
        List<String> list = listObjectNames(bucketName);
        if (!list.isEmpty()) {
            for (String objectName : list) {
                deleteObject(bucketName, objectName);
            }
        }
    }

    /**
     * 查询桶中所有的文件名
     *
     * @param bucketName 桶名
     * @return objectNames
     */
    public List<String> listObjectNames(String bucketName) {
        try {
            List<String> objectNameList = new ArrayList<>();
            if (bucketExists(bucketName)) {
                Iterable<Result<Item>> results = listObjects(bucketName, true);
                for (Result<Item> result : results) {
                    String objectName = result.get().objectName();
                    objectNameList.add(objectName);
                }
            }
            return objectNameList;
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException
                | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            log.error("查询存储桶{}中所有的文件名发生异常，异常信息：", bucketName, e);
            throw new RuntimeException("查询存储桶所有的文件名未知异常", e);
        }
    }

    /**
     * 查询桶的对象信息
     *
     * @param bucketName 桶名
     * @param recursive  是否递归查询
     * @return 桶的对象信息
     */
    public Iterable<Result<Item>> listObjects(String bucketName, boolean recursive) {
        return minioClient.listObjects(
                ListObjectsArgs.builder().bucket(bucketName).recursive(recursive).build());
    }

    /**
     * 上传文件到默认存储桶
     *
     * @param objectName 文件完整路径 比如：exampledir/exampleobject.txt
     * @param file       文件
     */
    public void upload(String objectName, MultipartFile file) {
        upload(BUCKET_DEFAULT_PRIVATE, objectName, file);
    }


    /**
     * 上传文件到指定存储桶
     *
     * @param bucketName 存储桶
     * @param objectName 文件完整路径 比如：exampledir/exampleobject.txt
     * @param file       文件
     */
    public void upload(String bucketName, String objectName, MultipartFile file) {
        try {
            String extension = FileUtil.getExtension(file);
            if (!FileUtil.isAllowedExtension(extension, fileUploadProperties.getAllowedExtension())) {
                throw new RuntimeException("上传文件失败，不允许上传文件类型为[" + extension + "]的文件！");
            }
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectName)
                            .stream(file.getInputStream(), file.getSize(), -1)
                            .build());
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException
                | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            log.error("上传文件发生异常，异常信息：", e);
            throw new RuntimeException("上传文件未知异常", e);
        }
    }

    /**
     * 文件是否存在(默认存储桶)
     *
     * @param objectName 不包含Bucket名称在内的Object完整路径
     */
    public boolean ObjectExist(String objectName) {
        return ObjectExist(BUCKET_DEFAULT, objectName);
    }


    /**
     * 文件是否存在
     *
     * @param bucketName 存储空间
     * @param objectName 不包含Bucket名称在内的Object完整路径
     * @return 是否存在 true：是 false：否
     */
    public boolean ObjectExist(String bucketName, String objectName) {
        try {
            StatObjectResponse statObjectResponse = minioClient.statObject(StatObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .build());
            return statObjectResponse != null;
        } catch (ErrorResponseException ee) {
            return false; //  文件不存在的时候会报这个异常，提示信息是 Object does not exist
        } catch (InsufficientDataException | InternalException | InvalidKeyException
                | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            log.error("查询存储桶{}的文件{}是否存在发生异常，异常信息：", bucketName, objectName, e);
            throw new RuntimeException("查询文件是否存在未知异常", e);
        }
    }

    /**
     * 文件下载
     *
     * @param objectName Object完整路径
     * @return 文件byte数组
     */
    public byte[] download(String objectName) {
        return download(BUCKET_DEFAULT, objectName);
    }


    /**
     * 文件下载
     *
     * @param bucketName 存储空间
     * @param objectName Object完整路径
     * @return 文件byte数组
     */
    public byte[] download(String bucketName, String objectName) {
        try {
            GetObjectResponse getObjectResponse = minioClient.getObject(
                    GetObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectName)
                            .build());
            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            int nRead;
            byte[] data = new byte[1024];
            while ((nRead = getObjectResponse.read(data, 0, data.length)) != -1) {
                buffer.write(data, 0, nRead);
            }
            buffer.flush();
            byte[] byteArray = buffer.toByteArray();
            getObjectResponse.close();

            return byteArray;
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException
                | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            log.error("下载存储桶{}的文件{}发生异常，异常信息：", bucketName, objectName, e);
            throw new RuntimeException("下载文件未知异常", e);
        }
    }


    /**
     * 删除文件（默认存储桶）
     *
     * @param objectName Object完整路径
     */
    public void deleteObject(String objectName) {
        deleteObject(BUCKET_DEFAULT, objectName);
    }


    /**
     * 删除文件
     *
     * @param bucketName 桶名
     * @param objectName Object完整路径
     */
    public void deleteObject(String bucketName, String objectName) {
        try {
            minioClient.removeObject(RemoveObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .build());
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException
                | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            log.error("删除存储桶{}的文件{}发生异常，异常信息：", bucketName, objectName, e);
            throw new RuntimeException("删除文件未知异常", e);
        }
    }

    /**
     * 大文件上传-01分片上传
     *
     * @param tempBucket 临时存储桶名称（可以使用文件MD5之后生成，但如果文件太大，这个过程会比较耗时）
     * @param chunk      大文件分片下标
     * @param file       下标对应的分片文件
     */
    public void chunkUpload(String tempBucket, Integer chunk, MultipartFile file) {
        String objectName = String.valueOf(chunk);
        if (!bucketExists(tempBucket)) {
            this.createBucket(tempBucket);
        }
        upload(tempBucket, objectName, file);
    }

    /**
     * 大文件上传-02合并
     *
     * @param tempBucket       临时存储桶名称
     * @param shardCount       分片总数量
     * @param targetBucketName 目标存储桶
     * @param targetObjectName 目标Object完整路径
     * @return 文件信息
     */
    public FileInfo merge(String tempBucket, Integer shardCount, String targetBucketName, String targetObjectName) {

        // 查询分片数据
        List<String> objectNameList = listObjectNames(tempBucket);
        if (shardCount != objectNameList.size()) {
            log.error("合并分片失败，分片数量校验失败");
            throw new RuntimeException("合并分片失败，分片数量校验失败");
        }

        FileInfo fileInfo = new FileInfo();
        fileInfo.setRelativeFilePath(targetObjectName);

        // 开始合并
        composeObject(tempBucket, targetBucketName, targetObjectName);
        log.info("存储桶：{} 中的分片文件，已经在目标存储桶合并成功，新文件为：{}", tempBucket, targetObjectName);

        // 合并成功之后删除对应的临时桶
        removeBucket(tempBucket, true);
        log.info("删除存储桶 {} 成功", tempBucket);

        return fileInfo;
    }


    /**
     * 文件合并，将分块文件组成一个新的文件
     *
     * @param originBucketName 分块文件所在的桶
     * @param targetBucketName 合并文件生成文件所在的桶
     * @param objectName       存储于桶中的对象名 原始文件名
     */
    private void composeObject(String originBucketName, String targetBucketName, String objectName) {
        try {
            List<String> objectNameList = listObjectNames(originBucketName);
            if (ObjectUtils.isEmpty(objectNameList)) {
                throw new IllegalArgumentException(originBucketName + "桶中没有文件，请检查");
            }
            // 分块文件集合
            List<ComposeSource> composeSourceList = new ArrayList<>(objectNameList.size());
            // 对文件名集合进行升序排序
            objectNameList.sort(Comparator.comparingInt(Integer::parseInt));
            for (String object : objectNameList) {
                composeSourceList.add(ComposeSource.builder()
                        .bucket(originBucketName)
                        .object(object)
                        .build());
            }
            minioClient.composeObject(ComposeObjectArgs.builder()
                    .bucket(targetBucketName)
                    .object(objectName)
                    .sources(composeSourceList)
                    .build());
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            log.error("大文件合并发生异常，异常信息：", e);
            throw new RuntimeException("大文件合并未知异常", e);
        }
    }

    /**
     * 获取文件上传配置
     *
     * @return 配置
     */
    public FileUploadProperties getFileUploadProperties() {
        return fileUploadProperties;
    }
}
